Skip to content

Track type inheritance chain assemblies in symbol-based mode#145

Merged
dfederm merged 1 commit intomainfrom
dfederm/track-inheritance-chain
Apr 30, 2026
Merged

Track type inheritance chain assemblies in symbol-based mode#145
dfederm merged 1 commit intomainfrom
dfederm/track-inheritance-chain

Conversation

@dfederm
Copy link
Copy Markdown
Owner

@dfederm dfederm commented Apr 30, 2026

Fixes #144.

In the symbol-based analysis path, only the immediate base type's containing assembly was credited via TrackType(namedType.BaseType). The C# compiler validates the entire base-type chain plus implemented interfaces at type-check time -- CS0012 fires when any link's defining assembly is missing -- so any assembly along the chain must remain a reference. With <DisableTransitiveProjectReferences>true</...>, the chain doesn't flow transitively, so the consumer must explicitly reference grandparent assemblies.

Symbol-based RT, when handling INamedTypeSymbol, walked only the immediate BaseType and direct Interfaces, never recursing further up. So for Consumer : Provider where Provider : ProviderDependency in another assembly, the consumer's reference to ProviderDependency was wrongly flagged RT0002 removable.

Fix: extend TrackType's INamedTypeSymbol branch to walk the full BaseType chain and AllInterfaces collection. AllInterfaces is broader than Interfaces (includes transitively-implemented interfaces) and mirrors what the compiler validates. A ConcurrentDictionary<ISymbol, byte> keyed via SymbolEqualityComparer.Default gates the chain walk to break self-referential cycles such as int -> AllInterfaces[IComparable<int>] -> typeArg int -> ... and to avoid redundant work.

Adds seven regression tests that all fail without this change:

  • UsedViaInheritedBaseType: the canonical issue [ReferenceTrimmerUseSymbolAnalysis] Account for type inheritance #144 scenario
  • UsedViaImplementedInterface: interface in the chain
  • UsedViaMultiLevelInheritanceChain: three-level A <- B <- C
  • UsedViaInheritanceChainOnVariableType: chain via parameter type
  • UsedViaMixedBaseAndInterfaceChain: base + interface, four asms
  • UsedViaGenericConstraintBaseChain: where T : Provider constraint
  • UnrelatedReferenceNotMarkedByInheritance: negative test ensuring the fix doesn't over-credit unrelated assemblies

Verified against upstream repro https://github.com/olstakh/RT_Gap_Inheritance: Consumer builds with 0 RT warnings (was emitting RT0002 with v3.5.2).

Different code path from #141 (qualifier tracking on inherited static member access), #142 (delegate parameter / return types), and #143 (override chains). Same family of "symbol traversal misses a chain" gaps; same reporter (olstakh).

In the symbol-based analysis path, only the immediate base type's
containing assembly was credited via `TrackType(namedType.BaseType)`.
The C# compiler validates the entire base-type chain plus implemented
interfaces at type-check time -- CS0012 fires when *any* link's defining
assembly is missing -- so any assembly along the chain must remain a
reference. With `<DisableTransitiveProjectReferences>true</...>`, the
chain doesn't flow transitively, so the consumer must explicitly
reference grandparent assemblies.

Symbol-based RT, when handling `INamedTypeSymbol`, walked only the
immediate `BaseType` and direct `Interfaces`, never recursing further
up. So for `Consumer : Provider` where `Provider : ProviderDependency`
in another assembly, the consumer's reference to `ProviderDependency`
was wrongly flagged RT0002 removable.

Fix: extend `TrackType`'s `INamedTypeSymbol` branch to walk the full
`BaseType` chain and `AllInterfaces` collection. `AllInterfaces` is
broader than `Interfaces` (includes transitively-implemented interfaces)
and mirrors what the compiler validates. A `ConcurrentDictionary<ISymbol,
byte>` keyed via `SymbolEqualityComparer.Default` gates the chain walk
to break self-referential cycles such as `int -> AllInterfaces[IComparable<int>]
-> typeArg int -> ...` and to avoid redundant work.

Adds seven regression tests that all fail without this change:
- `UsedViaInheritedBaseType`: the canonical issue #144 scenario
- `UsedViaImplementedInterface`: interface in the chain
- `UsedViaMultiLevelInheritanceChain`: three-level A <- B <- C
- `UsedViaInheritanceChainOnVariableType`: chain via parameter type
- `UsedViaMixedBaseAndInterfaceChain`: base + interface, four asms
- `UsedViaGenericConstraintBaseChain`: `where T : Provider` constraint
- `UnrelatedReferenceNotMarkedByInheritance`: negative test ensuring
  the fix doesn't over-credit unrelated assemblies

Verified against upstream repro https://github.com/olstakh/RT_Gap_Inheritance:
`Consumer` builds with 0 RT warnings (was emitting RT0002 with v3.5.2).

Different code path from #141 (qualifier tracking on inherited static
member access), #142 (delegate parameter / return types), and #143
(override chains). Same family of "symbol traversal misses a chain"
gaps; same reporter (olstakh).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dfederm dfederm enabled auto-merge (squash) April 30, 2026 21:17
@dfederm dfederm merged commit d7b476c into main Apr 30, 2026
2 checks passed
@dfederm dfederm deleted the dfederm/track-inheritance-chain branch April 30, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ReferenceTrimmerUseSymbolAnalysis] Account for type inheritance

1 participant